package com.progenies.ecg.asm31.model.factory;

import org.objectweb.asm.ClassVisitor;
import org.objectweb.asm.Type;

import com.progenies.ecg.asm31.model.codeparts.lines.InvokeMethodLinePart;
import com.progenies.ecg.asm31.util.GenerationUtils;
import com.progenies.ecg.generator.ClassGeneratorFactory;
import com.progenies.ecg.model.ConstructorObject;
import com.progenies.ecg.model.FieldObject;
import com.progenies.ecg.model.MethodObject;
import com.progenies.ecg.model.ParameterObject;
import com.progenies.ecg.model.codeparts.CodeBlock;
import com.progenies.ecg.model.codeparts.lines.LineFactory;
import com.progenies.ecg.model.factory.ObjectFactory;

/**
 * Represents a Field Object, in the specific implementation of ASM3.1
 * @author ofc587a87
 * @see FieldObject
 */
class FieldObjectASM31<T> extends FieldObject<T>
{
	private ClassVisitor visitor;
	private ClassObjectASM31 parent;
	
	
	FieldObjectASM31(String name, Class<T> fieldType, int modifiers[], boolean generateSetter, boolean generateGetter)
	{
		this.name=name;
		this.classType=fieldType;
		this.modifiers=modifiers;
		this.generateSetter=generateSetter;
		this.generateGetter=generateGetter;
	}

	/* (non-Javadoc)
	 * @see com.progenies.ecg.model.AbstractCodeObject#generateBytecode()
	 */
	@Override
	public void generateBytecode() {
		if(this.visitor==null)
			throw new IllegalStateException("Field has not been configurated, please use ClassGenerator");

		//creates the instance field
		if((this.value!=null && this.value.getClass().isArray())) //if it's an array, i'll recreate it at constructor
			visitor.visitField(GenerationUtils.getAccessTranslated(this.modifiers), name, Type.getDescriptor(classType), null, null);
		else
		{
			Object tmpValue=(this.value instanceof Boolean)?(((Boolean)this.value).booleanValue()?1:0):this.value;
			visitor.visitField(GenerationUtils.getAccessTranslated(this.modifiers), name, Type.getDescriptor(classType), null, tmpValue);
		}
		
		//if the field has a value, and is not static, we must modify every constructor to add the set instruction
		//Or also if it's marked as "initialize"
		//TODO: Can't initialize primitives!!! -> a personalized exception should be thrown
		if((this.value!=null && !GenerationUtils.isStatic(this.modifiers)) || this.initialize || this.varReference!=null || this.matrixVarReference!=null)
		{
			for(ConstructorObject contr : this.parent.getConstructors())
			{
				//don't modify static initializer
				if(contr.getName().equals("<clinit>"))
					continue;
			
				CodeBlock codeBlock=getCodeBlock(contr);
				
				//i want to add the setValue at the start of the constructor, but not if it's a call to other constructor!
				int indexToInsert=getIndexToInsert(codeBlock);
				
				//I'll insert the setValue at 'indexToInsert', that will be the first or second line
				//TODO: Check if the static value is really a static value (no object, etc)
				if(this.value!=null)
				{
					if(this.value.getClass().isArray())
					{
						codeBlock.add(indexToInsert, ClassGeneratorFactory.getClassGenerator().lineFactory.variables.createArrayAndStore(classType, this.value, this.name));
					}
					else
					{
						codeBlock.add(indexToInsert, ClassGeneratorFactory.getClassGenerator().lineFactory.variables.setVariableValue(this.name, this.value));
					}
				}
				else if(this.initialize && this.varReference==null)
				{
					if(this.arrayDimensions==null || this.arrayDimensions.length==0)
						codeBlock.add(indexToInsert, ClassGeneratorFactory.getClassGenerator().lineFactory.objects.createObjectAndStore(this.classType, this.initializationParams, this.name));
					else
						codeBlock.add(indexToInsert, ClassGeneratorFactory.getClassGenerator().lineFactory.variables.createEmptyArrayAndStore(this.classType, this.arrayDimensions, this.name));
				}
				else if(this.varReference!=null)
				{
					codeBlock.add(indexToInsert, ClassGeneratorFactory.getClassGenerator().lineFactory.variables.createArrayAndStore(classType, name, varReference));
				}
				else if(matrixVarReference!=null)
				{
					codeBlock.add(indexToInsert, ClassGeneratorFactory.getClassGenerator().lineFactory.variables.createArrayAndStore(classType, name, matrixVarReference));
				}
				
			}
		}
		else if(this.value!=null && GenerationUtils.isStatic(this.modifiers) && this.value.getClass().isArray()) //add static initialization for static array fields 
		{
			//search static initializer (only one per class!!)
			ConstructorObject staticContr=null;
			for(ConstructorObject contr : this.parent.getConstructors())
			{
				if(contr.getName().equals("<clinit>"))
				{
					staticContr=contr;
					break;
				}
			}
				
			//if not found, create one
			if(staticContr==null)
			{
				staticContr=ClassGeneratorFactory.getClassGenerator().objectFactory.constructors.createConstructor();
				staticContr.setName("<clinit>");
				this.parent.getConstructors().add(staticContr);
			}
			
			
			CodeBlock codeBlock=getCodeBlock(staticContr);
			
			//i want to add the setValue at the start of the constructor, but not if it's a call to other constructor!
			int indexToInsert=getIndexToInsert(codeBlock);
			
			codeBlock.add(indexToInsert, ClassGeneratorFactory.getClassGenerator().lineFactory.variables.createArrayAndStore(classType, this.value, this.name));
	
		}

		//if there are getters and/or setters, we'll create and add them to the class model
		if(this.generateGetter || this.generateSetter)
		{
			String propertyName=this.name.substring(0, 1).toUpperCase() + this.name.substring(1);
			
			ObjectFactory objFactory=ClassGeneratorFactory.getClassGenerator().objectFactory;
			LineFactory lineFactory=ClassGeneratorFactory.getClassGenerator().lineFactory;
			
			if(this.generateGetter)
			{
				MethodObject getter=objFactory.methods.createMethod("get"+propertyName, (ParameterObject<?>[])null, classType);
				getter.setCode(lineFactory.methods.returnVariableValue(name));
				parent.getMethods().add(getter);
			}
			
			if(this.generateSetter)
			{
				MethodObject getter=objFactory.methods.createMethod("set"+propertyName, new ParameterObject<?>[] {new ParameterObject<T>("localValue", classType)});
				getter.setCode(lineFactory.createCodeBlock());
				((CodeBlock)getter.getCode()).add(lineFactory.variables.setVariableValueFromVariable(name, "localValue"));
				((CodeBlock)getter.getCode()).add(lineFactory.methods.returnEmpty());
				parent.getMethods().add(getter);
			}

			
		}


	}

	
	/**
	 * Configures the field object for bytecode generation
	 * @param visitor ClassVisitor object
	 * @param varRegister 
	 */
	void configure(ClassVisitor visitor, ClassObjectASM31 parent)
	{
		this.visitor=visitor;
		this.parent=parent;
	}
	
	
	
	private CodeBlock getCodeBlock(ConstructorObject contr)
	{
		//gets the CodeBlock. If it's not a CodeBlock, just create one and add the original instruction
		CodeBlock codeBlock=null;
		if(contr.getCode()==null)
		{
			codeBlock=ClassGeneratorFactory.getClassGenerator().lineFactory.createCodeBlock();
			contr.setCode(codeBlock);
		}
		else if(!(contr.getCode() instanceof CodeBlock))
		{
			codeBlock=ClassGeneratorFactory.getClassGenerator().lineFactory.createCodeBlock();
			codeBlock.add(contr.getCode());
			contr.setCode(codeBlock);
		}
		else
			codeBlock=(CodeBlock) contr.getCode();
		
		return codeBlock;
	}
	
	
	private int getIndexToInsert(CodeBlock codeBlock)
	{
		int indexToInsert=0;
		if(codeBlock.size()>0 && codeBlock.get(0) instanceof InvokeMethodLinePart<?>)
		{
			InvokeMethodLinePart<?> firstLine=(InvokeMethodLinePart<?>) codeBlock.get(0);
			
			//Only insert if the init invoke the super class, not if the init invokes other constructor from this class
			if(firstLine.getMethodName().startsWith("<") && firstLine.getOwnerClass()==null)
				indexToInsert++;
		}
		return indexToInsert;
	}

}
